package com.ocm.core.service.jackrabbit;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.Repository;
import javax.jcr.Session;
import javax.jcr.SimpleCredentials;
import javax.jcr.Value;
import javax.jcr.version.Version;
import javax.jcr.version.VersionHistory;
import javax.jcr.version.VersionIterator;
import javax.jcr.version.VersionManager;

import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.rmi.repository.URLRemoteRepository;
import org.springframework.stereotype.Service;

import com.ocm.core.constant.OCMConstant;
import com.ocm.core.domain.AssociationType;
import com.ocm.core.domain.PropertyDefinition;
import com.ocm.core.exception.OCMException;
import com.ocm.core.factory.ObjectFactory;
import com.ocm.core.model.ClassInstance;
import com.ocm.core.model.ReferenceValue;
import com.ocm.core.service.AssociationTypeService;
import com.ocm.core.service.JCRSecurityService;
import com.ocm.core.service.JCRService;

@Service("jcrService")
public class JCRServiceImpl implements JCRService {

   public Session login(String username, String password, String connectionURL) throws Exception {
      Repository repository = new URLRemoteRepository(connectionURL);
      Session session = repository.login(new SimpleCredentials(username, password.toCharArray()));
      return session;
   }

   public void logout(Session session) throws Exception {
      session.logout();
   }

   public Node addNodeHasVersion(Session session, String nodeId, String nodeName, Map<String, Object> properties)
         throws Exception {
      Node root = session.getRootNode();
      Node parentNode = root;
      if (nodeId != null) {
         parentNode = session.getNodeByIdentifier(nodeId);
      }
      Node newNode = parentNode.addNode(nodeName);
      newNode.addMixin(JcrConstants.MIX_VERSIONABLE);
      newNode.addMixin(JcrConstants.MIX_REFERENCEABLE);
      session.save();
      String newNodeId = newNode.getIdentifier();
      checkout(session, newNodeId);
      checkin(session, newNodeId, properties);
      return newNode;
   }

   public Node addNode(Session session, String nodeId, String nodeName, Map<String, Object> properties)
         throws Exception {
      Node root = session.getRootNode();
      Node parentNode = root;
      if (nodeId != null) {
         parentNode = session.getNodeByIdentifier(nodeId);
      }
      Node newNode = parentNode.addNode(nodeName);
      newNode.addMixin(JcrConstants.MIX_REFERENCEABLE);
      setProperties(newNode, properties);
      session.save();
      return newNode;
   }

   public Node addNode(Session session, String nodeName) throws Exception {
      Node root = session.getRootNode();
      Node newNode = root.addNode(nodeName);
      session.save();
      return newNode;
   }

   public void removeNode(Session session, String id) throws Exception {
      Node oldNode = session.getNodeByIdentifier(id);
      removeReferenceValue(oldNode);
      oldNode.remove();
      session.save();
   }

   public Map<PropertyDefinition, Object> getValues(Session session, String nodeId, List<PropertyDefinition> properties)
         throws Exception {
      Node node = session.getNodeByIdentifier(nodeId);
      Map<PropertyDefinition, Object> propertiesMap = getPropertiesValue(node, node, properties, false);
      return propertiesMap;
   }

   public ClassInstance getClassInstance(Session session, String nodeId, List<PropertyDefinition> properties, boolean hasSecurity) throws Exception {
      ClassInstance classInstance = new ClassInstance();
      Node node = session.getNodeByIdentifier(nodeId);
      Map<PropertyDefinition, Object> propertiesMap = getPropertiesValue(node, node, properties, false);
      if (hasSecurity){
         classInstance.setJcrNodeSecurityObj(getSecurityValue(node));
      }
      classInstance.setProperties(propertiesMap);
      return classInstance;
   }

   public void removeNodeByPath(Session session, String nodePath) throws Exception {
      Node oldNode = session.getNode(nodePath);
      oldNode.remove();
      session.save();
   }

   public void checkout(Session session, String nodeId) throws Exception {
      Node node = session.getNodeByIdentifier(nodeId);
      VersionManager versionManager = session.getWorkspace().getVersionManager();
      versionManager.checkout(node.getPath());
      session.save();
   }

   @SuppressWarnings("deprecation")
   public void cancelCheckout(Session session, String nodeId) throws Exception {
      Node node = session.getNodeByIdentifier(nodeId);
      node.restore(node.getBaseVersion(), true);
   }

   @SuppressWarnings("deprecation")
   public void removeLastVersion(Session session, String nodeId) throws Exception {
      Node node = session.getNodeByIdentifier(nodeId);
      VersionManager versionManager = session.getWorkspace().getVersionManager();
      VersionHistory versionHistory = versionManager.getVersionHistory(node.getPath());
      VersionIterator it = versionHistory.getAllVersions();
      it.skip(it.getSize() - 2);
      Version earlyVs = (Version) it.next();
      versionManager.checkout(node.getPath());
      node.restore(earlyVs, true);
      Version newlyVs = (Version) it.next();
      versionHistory.removeVersion(newlyVs.getName());
   }

   public void checkin(Session session, String nodeId, Map<String, Object> properties) throws Exception {
      Node node = session.getNodeByIdentifier(nodeId);
      setProperties(node, properties);
      VersionManager versionManager = session.getWorkspace().getVersionManager();
      session.save();
      versionManager.checkin(node.getPath());
   }

   public void updateNode(Session session, String nodeId, Map<String, Object> properties) throws Exception {
      Node node = session.getNodeByIdentifier(nodeId);
      setProperties(node, properties);
      session.save();
   }

   public void updateSecurity(Session session, String nodeId, String securityObject) throws Exception {
      Node node = session.getNodeByIdentifier(nodeId);
      processSecurityValue(node, securityObject, false);
      session.save();
   }

   public void updateSecurityHasVersionable(Session session, String nodeId, String securityObject) throws Exception {
      Node node = session.getNodeByIdentifier(nodeId);
      processSecurityValue(node, securityObject, true);
      session.save();
   }
   
   public List<Map<PropertyDefinition, Object>> historyNode(Session session, String nodeId,
         List<PropertyDefinition> properties) throws Exception {
      Node node = session.getNodeByIdentifier(nodeId);
      VersionManager versionManager = session.getWorkspace().getVersionManager();
      VersionHistory versionHistory = versionManager.getVersionHistory(node.getPath());
      List<Map<PropertyDefinition, Object>> propertiesList = new ArrayList<Map<PropertyDefinition, Object>>();
      VersionIterator it = versionHistory.getAllVersions();
      it.skip(1);
      while (it.hasNext()) {
         Version version = it.nextVersion();
         NodeIterator nodeIterator = version.getNodes();
         Node currentNode = nodeIterator.nextNode();
         Map<PropertyDefinition, Object> propertiesMap = getPropertiesValue(version, currentNode, properties, false);
         propertiesList.add(propertiesMap);
      }
      return propertiesList;
   }

   public Node getNodeId(Session session, String nodeId) throws Exception {
      Node node = session.getNodeByIdentifier(nodeId);
      return node;
   }

   public Object getPropertyValue(Session session, String nodeId, PropertyDefinition propertyDefinition)
         throws Exception {
      Node node = session.getNodeByIdentifier(nodeId);
      List<PropertyDefinition> propertyDefinitions = new ArrayList<PropertyDefinition>();
      propertyDefinitions.add(propertyDefinition);
      Map<PropertyDefinition, Object> propertiesValue = getPropertiesValue(node, node, propertyDefinitions, true);
      Object value = propertiesValue.get(propertyDefinition);
      return value;
   }

   public Map<PropertyDefinition, Object> getValues(Node node, List<PropertyDefinition> properties) throws Exception {
      Map<PropertyDefinition, Object> values = getPropertiesValue(node, node, properties, false);
      return values;
   }

   private Map<PropertyDefinition, Object> getPropertiesValue(Node parentNode, Node versionNode,
         List<PropertyDefinition> properties, boolean hasPropertyObject) throws Exception {
      Map<PropertyDefinition, Object> propertiesMap = new HashMap<PropertyDefinition, Object>();
      for (PropertyDefinition propertyDefinition : properties) {
         String propertyName = propertyDefinition.getName();
         if (propertyName.equalsIgnoreCase(OCMConstant.IS_CHECKEDOUT)) {
            propertiesMap.put(propertyDefinition, parentNode.isCheckedOut());
         }else if (propertyName.equalsIgnoreCase(OCMConstant.VERSION_NUMBER)) {
            propertiesMap.put(propertyDefinition, parentNode.getName());
         } else if (versionNode.hasProperty(propertyName)) {
            Object value = propertyDefinition.getPropertyType().convertToJavaType(
                  versionNode.getProperty(propertyName), hasPropertyObject, propertyDefinition.getObjectConfig(), this);
            propertiesMap.put(propertyDefinition, value);
         } else {
            propertiesMap.put(propertyDefinition, null);
         }
      }
      return propertiesMap;
   }

   private void setProperties(Node node, Map<String, Object> properties) throws Exception {
      if (properties != null) {
         Iterator<String> it = properties.keySet().iterator();
         while (it.hasNext()) {
            String propertyName = it.next();
            Object value = properties.get(propertyName);
            if (OCMConstant.SYSTEM_SECURITY_PROPERTY.equals(propertyName)) {
               processSecurityValue(node, (String) value, false);
            } else if (value instanceof Value) {
               node.setProperty(propertyName, (Value) value);
            } else if (value instanceof ReferenceValue) {
               processReferenceValue(node, propertyName, (ReferenceValue) value);
            }
         }
      }
   }

   private void removeReferenceValue(Node removeNode) throws Exception {
      PropertyIterator propertyIterator = removeNode.getReferences();
      while (propertyIterator.hasNext()){
         Property property = propertyIterator.nextProperty();
         List<AssociationTypeService> associationTypeServices = getAssociationTypeServices(property);
         for (AssociationTypeService associationTypeService : associationTypeServices){
            associationTypeService.removeReference(property, removeNode);
         }
      }
   }

   private void processSecurityValue(Node instanceClass, String securityObj, boolean hasVersion) throws Exception {
      JCRSecurityService jcrSecurityService = ObjectFactory.getJCRSecurityService();
      jcrSecurityService.processSecurity(instanceClass, OCMConstant.SYSTEM_SECURITY_PROPERTY, securityObj, hasVersion);
   }

   private String getSecurityValue(Node instanceClass) throws Exception {
      JCRSecurityService jcrSecurityService = ObjectFactory.getJCRSecurityService();
      String securityObj = jcrSecurityService.getSecurity(instanceClass, OCMConstant.SYSTEM_SECURITY_PROPERTY);
      return securityObj;
   }

   private void processReferenceValue(Node instanceClass, String propertyName, ReferenceValue referenceValue)
         throws Exception {
      AssociationTypeService associationTypeService = referenceValue.getAssociationType().getAssociationTypeService(this);
      associationTypeService.processReferenceValue(instanceClass, propertyName, referenceValue);
   }

   private List<AssociationTypeService> getAssociationTypeServices(Property property) throws OCMException{
      List<AssociationTypeService> services = AssociationType.associationTypeServices;
      List<AssociationTypeService> associationTypeServices = new ArrayList<AssociationTypeService>();;
      for (AssociationTypeService associationTypeService : services){
         if (associationTypeService.support(property)){
            associationTypeServices.add(associationTypeService);
         }
      }
      return associationTypeServices;
   }
}
